<?php

namespace app\common;

use think\exception\Handle;
use think\exception\HttpException;
use Exception;
use think\App;
use think\Config;
use think\Response;

class ExceptionHandle extends Handle
{
    // 错误报告目录
    const REPORT_PATH = RUNTIME_PATH . 'report' . DS;

    /**
     * @param Exception $exception
     * @return Response
     */
    protected function convertExceptionToResponse(Exception $exception)
    {
        // 存下原有的debug（貌似是个对象来着？）
        $debug = App::$debug;
        // 防止原有模板不渲染
        App::$debug = true;
        // 获取详细的错误信息
        $data = $this->getDetailedData($exception);
        // 保存echo到临时变量
        $dataEcho = $this->getEcho();
        // 将echo存入data
        $data['echo'] = $dataEcho;
        // 获取详细错误报告内容并清空缓存
        $content = $this->toContent($data);
        // 保存到文件
        $errorFileName = $this->saveToFile($content);

        if (!$debug) {
            // 重新获取基本错误信息
            $data = $this->getBasicData($exception);
            // 显示文件名
            $data['message'] = '页面错误：' . $errorFileName;
            // 将临时变量echo再次存入
            $data['echo'] = $dataEcho;
            // 还原debug，防止渲染异常
            App::$debug = $debug;
            // 获取基本错误报告（只有文件名）并清空缓存
            $content = $this->toContent($data);
        }

        $response = new Response($content, 'html');

        if ($exception instanceof HttpException) {
            $statusCode = $exception->getStatusCode();
            $response->header($exception->getHeaders());
        }

        if (!isset($statusCode)) {
            $statusCode = 500;
        }
        $response->code($statusCode);
        return $response;
    }

    /**
     * 从异常信息中过滤出经常看的那些参数
     */
    private static function filteData($data)
    {
        $text = "{$data['echo']}\n";
        $text .= "{$data['name']}\n";
        $text .= "in {$data['file']}\n";
        $text .= "line {$data['line']}\n";
        $text .= "{$data['message']}\n";
        $lineno = $data['source']['first'];
        foreach ($data['source']['source'] as $code) {
            $text .= "$lineno | $code"; // code自带回车，不需要另外添加
            $lineno++;
        }
        return $text;
    }

    /**
     * 获取echo
     * @return echo数据
     */
    private static function getEcho()
    {
        //保留一层
        while (ob_get_level() > 1) {
            ob_end_clean();
        }

        return ob_get_clean();
    }

    /**
     * 获取详细错误信息
     * @param Exception $exception
     * @return array 详细错误信息
     */
    private function getDetailedData(Exception $exception)
    {
        return [
            'name'    => get_class($exception),
            'file'    => $exception->getFile(),
            'line'    => $exception->getLine(),
            'message' => $this->getMessage($exception),
            'trace'   => $exception->getTrace(),
            'code'    => $this->getCode($exception),
            'source'  => $this->getSourceCode($exception),
            'datas'   => $this->getExtendData($exception),
            'tables'  => [
                'GET Data'              => $_GET,
                'POST Data'             => $_POST,
                'Files'                 => $_FILES,
                'Cookies'               => $_COOKIE,
                'Session'               => isset($_SESSION) ? $_SESSION : [],
                'Server/Request Data'   => $_SERVER,
                'Environment Variables' => $_ENV,
                'ThinkPHP Constants'    => $this->getConst(),
            ],
        ];
    }

    /**
     * 获取基本错误信息 Code 和 Message
     * @param Exception $exception
     * @return array 基本错误信息
     */
    private function getBasicData(Exception $exception)
    {
        return [
            'code'    => $this->getCode($exception),
            'message' => $this->getMessage($exception),
        ];
    }

    /**
     * 将错误信息保存到文件
     * @param string $content 错误信息内容
     * @return string 错误文件路径
     */
    private static function saveToFile($content)
    {
        $errorFileName = strval(time()) . rand(10000, 99999);
        $errorFilePath = self::REPORT_PATH . $errorFileName . '.html';
        !is_dir(self::REPORT_PATH) && mkdir(self::REPORT_PATH, 0755, true);
        $errorFile = fopen($errorFilePath, "w");
        fwrite($errorFile, $content);
        return $errorFileName;
    }

    /**
     * 将错误信息转为字符串
     * @param array $data 错误数据
     * @return string 错误信息字符串
     */
    private static function toContent($data)
    {
        ob_start();
        extract($data);

        include Config::get('exception_tmpl');
        // 获取并清空缓存
        return ob_get_clean();
    }

    /**
     * 获取常量列表
     * @return array 常量列表
     */
    private static function getConst()
    {
        return get_defined_constants(true)['user'];
    }
}